import os
import signal
import time
import unittest
import psutil
import subprocess
from unittest import mock
from multiprocessing import Process
from multiprocessing.process import BaseProcess

from engine_server.utils.proc import get_child_processes, exit_process, kill_process_tree


def create_test_process(sleep_sec: int = 10) -> Process:
    def run():
        time.sleep(sleep_sec)
    
    proc = Process(target=run)
    proc.start()
    while not proc.is_alive():
        time.sleep(0.1)
    return proc

def create_process_with_children() -> Process:
    def parent_run():
        child = subprocess.Popen(["sleep", "20"])
        child.wait()
    
    proc = Process(target=parent_run)
    proc.start()
    time.sleep(0.5)
    return proc

class TestProc(unittest.TestCase):
    
    @mock.patch('psutil.Process')
    def test_get_child_processes_alive(self, mock_psutil_process):
        mock_base_process = mock.MagicMock(spec=BaseProcess)
        mock_base_process.is_alive.return_value = True
        mock_base_process.pid = 1234
        
        mock_process_instance = mock_psutil_process.return_value
        mock_child1 = mock.MagicMock()
        mock_child2 = mock.MagicMock()
        mock_process_instance.children.return_value = [mock_child1, mock_child2]
        
        result = get_child_processes([mock_base_process])

        self.assertEqual(result, [mock_child1, mock_child2])
        mock_psutil_process.assert_called_once_with(1234)
        mock_process_instance.children.assert_called_once_with(recursive=False)
    
    def test_get_child_processes_not_alive(self):
        mock_base_process = mock.MagicMock(spec=BaseProcess)
        mock_base_process.is_alive.return_value = False
        
        result = get_child_processes([mock_base_process])
        
        self.assertEqual(result, [])
    
    @mock.patch('psutil.Process')
    def test_get_child_processes_recursive(self, mock_psutil_process):
        mock_base_process = mock.MagicMock(spec=BaseProcess)
        mock_base_process.is_alive.return_value = True
        mock_base_process.pid = 1234
        
        mock_process_instance = mock_psutil_process.return_value
        mock_child = mock.MagicMock()
        mock_process_instance.children.return_value = [mock_child]
        
        result = get_child_processes([mock_base_process], recursive=True)
        
        mock_process_instance.children.assert_called_once_with(recursive=True)
    
    @mock.patch('psutil.Process')
    def test_get_child_processes_exception(self, mock_psutil_process):
        mock_base_process = mock.MagicMock(spec=BaseProcess)
        mock_base_process.is_alive.return_value = True
        mock_base_process.pid = 1234
        
        mock_psutil_process.side_effect = psutil.NoSuchProcess(1234)
        
        result = get_child_processes([mock_base_process])
        
        self.assertEqual(result, [])
    
    
    @mock.patch('os.kill')
    @mock.patch('contextlib.suppress')
    @mock.patch('psutil.Process')
    def test_kill_process_tree(self, mock_psutil_process, mock_suppress, mock_kill):
        mock_child1 = mock.MagicMock()
        mock_child1.pid = 2001
        mock_child2 = mock.MagicMock()
        mock_child2.pid = 2002
        
        mock_parent = mock_psutil_process.return_value
        mock_parent.children.return_value = [mock_child1, mock_child2]
        
        kill_process_tree(1000)
        
        expected_calls = [mock.call(2001, signal.SIGKILL), 
                          mock.call(2002, signal.SIGKILL), 
                          mock.call(1000, signal.SIGKILL)]
        mock_kill.assert_has_calls(expected_calls)
    
    @mock.patch('psutil.Process')
    def test_kill_process_tree_no_such_process(self, mock_psutil_process):
        mock_psutil_process.side_effect = psutil.NoSuchProcess(1000)
        
        kill_process_tree(1000)

        mock_psutil_process.assert_called_once_with(1000)
    
    def test_exit_process_normal_termination(self):
        proc = create_test_process(sleep_sec=0.1)
        time.sleep(0.2)
        assert not proc.is_alive()
        
        exit_process([proc])
        assert not proc.is_alive()

    def test_exit_process_terminate_success(self):
        proc = create_test_process()
        assert proc.is_alive()
        
        exit_process([proc])
        
        assert not proc.is_alive()
        proc.join()
        assert proc.exitcode == -signal.SIGTERM


    def test_exit_process_no_pid(self):
        proc = Process(target=lambda: time.sleep(1))
        exit_process([proc])


    def test_exit_process_multiple_procs(self):
        procs = [create_test_process(), create_test_process()]
        assert all(p.is_alive() for p in procs)
        
        exit_process(procs)
        
        assert all(not p.is_alive() for p in procs)


if __name__ == '__main__':
    unittest.main()